#include "SystemSemaphore.h"
#include <thread>
#include <errno.h>

#if defined(_WIN32) || defined(WIN32) || defined(_WIN64) || defined(WIN64)
#define SEM_OPEN(key, value) ::CreateSemaphoreA(0, value, MAXLONG, (LPCSTR)key)
#define SEM_CLOSE(semaphore) ::CloseHandle(semaphore)
#define SEM_RELEASE(semaphore, count) ::ReleaseSemaphore(semaphore, count, 0)
#define SEM_WAIT(semaphore) ::WaitForSingleObjectEx(semaphore, INFINITE, FALSE)
#define SEM_UNLINK(key)
#endif

#if defined(__APPLE__) || defined(__linux__)
#include <sys/types.h>
#include <fcntl.h>

static inline bool ReleaseSemaphore(handle_t semaphore, int count)
{
    int cnt = count;
    do
    {
        if (::sem_post(semaphore) == -1)
        {
            // rollback changes to preserve the SysV semaphore behavior
            for (; cnt < count; ++cnt)
            {
                int res = 0;
                do
                {
                    res = ::sem_wait(semaphore);
                } while (res == -1 && errno == EINTR);
            }
            return false;
        }
        --cnt;
    } while (cnt > 0);
    return true;
}

#define SEM_OPEN(key, value) ::sem_open(key, O_CREAT | O_EXCL, 0600, value)
#define SEM_CLOSE(semaphore) ::sem_close(semaphore)
#define SEM_RELEASE(semaphore, count) ReleaseSemaphore(semaphore, count)
#define SEM_WAIT(semaphore) ::sem_wait(semaphore)
#define SEM_UNLINK(key) ::sem_unlink(key)

#endif

SystemSemaphore::SystemSemaphore(const std::string &key, int initialValue, SystemSemaphore::AccessMode mode)
    : name(key),
      initialValue(initialValue),
      mode(mode)
{
    this->OpenSemaphore();
}

SystemSemaphore::~SystemSemaphore()
{
    CloseSemaphore();
}

bool SystemSemaphore::Acquire()
{
    return ModifySemaphore(-1);
}

bool SystemSemaphore::Release(int n)
{
    if (n == 0)
        return true;
    if (n < 0)
    {
        return false;
    }
    return ModifySemaphore(n);
}

bool SystemSemaphore::OpenSemaphore()
{
    if (name.empty())
    {
        return false;
    }
    CloseSemaphore();

    if (nullptr == handle)
    {
#if defined(_WIN32) || defined(_WIN64)
        handle = SEM_OPEN(name.c_str(), initialValue);
#endif
#if defined(__APPLE__) || defined(__linux__)
        int oflag = O_CREAT | O_EXCL;
        for (int tryNum = 0, maxTries = 1; tryNum < maxTries; ++tryNum)
        {
            do
            {
                handle = ::sem_open(name.c_str(), oflag, 0600, initialValue);
            } while (handle == SEM_FAILED && errno == EINTR);
            if (handle == SEM_FAILED && errno == EEXIST)
            {
                if (mode == SystemSemaphore::Create)
                {
                    if (::sem_unlink(name.c_str()) == -1 && errno != ENOENT)
                    {
                        handle = nullptr;
                        break;
                    }
                    // Race condition: the semaphore might be recreated before
                    // we call sem_open again, so we'll retry several times.
                    maxTries = 3;
                }
                else
                {
                    // Race condition: if it no longer exists at the next sem_open
                    // call, we won't realize we created it, so we'll leak it later.
                    oflag &= ~O_EXCL;
                    maxTries = 2;
                }
            }
            else
            {
                break;
            }
        }
        if (handle == SEM_FAILED)
        {
            handle = nullptr;
        }

        createdSemaphore = (oflag & O_EXCL) != 0;
#endif
    }
    return handle != nullptr;
}

void SystemSemaphore::CloseSemaphore()
{
    if (handle)
    {
        SEM_CLOSE(handle);
        if (createdSemaphore)
        {
            SEM_UNLINK(name.c_str());
        }
        name = "";
        handle = nullptr;
    }
}

bool SystemSemaphore::ModifySemaphore(int count)
{
    if (nullptr == handle)
        return false;

    if (count > 0)
    {
        if (0 == SEM_RELEASE(handle, count))
        {
            return false;
        }
    }
    else
    {
        if (0 != SEM_WAIT(handle))
        {
            return false;
        }
    }

    return true;
}